Skip to content

9.23 审计日志

9.23.1 审计日志

在一个企业应用系统中,用户对系统所有的操作包括请求、数据库操作等等都应该记录起来,那么这些日志我们称为操作日志,也可以说审计日志。

通常来说,我们审计日志更多指的是数据库的操作记录

审计日志一般会记录以下三个操作:

  • 新增操作:记录某某人在某某时间对哪个表新增了什么数据
  • 更新操作:记录某某人在某某时间对哪个表的哪些数据做了更改,记录更改前的值和更改后的值
  • 删除操作:记录某某人在某某时间对哪个表删除了什么数据

9.23.2 关于 SaveChanges 事件

Furion 框架中为所有 AppDbContext 子类都提供了三个可重写的方法,这三个方法分别由三个事件触发:

  • 提交更改之前 SavingChanges 事件:触发 void SavingChangesEvent(DbContextEventData eventData, InterceptionResult<int> result) 方法
  • 提交更改之后 SavedChanges 事件:触发 void SavedChangesEvent(SaveChangesCompletedEventData eventData, int result) 方法
  • 提交更改失败 SaveChangesFailed 事件:触发 void SaveChangesFailedEvent(DbContextErrorEventData eventData) 方法

通过这三个事件我们就可以捕获所有更改的实体然后保存到数据库审计日志中。

9.23.3 如何实现

9.23.3.1 数据库审计日志

我们只需要在 AppDbContext 子类中重写 SavingChanges 事件对应方法即可:

using Furion.DatabaseAccessor;  
using Microsoft.AspNetCore.Http;  
using Microsoft.EntityFrameworkCore;  
using Microsoft.EntityFrameworkCore.Infrastructure;  
using System;  
using System.Linq;  
using System.Security.AccessControl;  

namespace Furion.EntityFramework.Core  
{  
    [AppDbContext("Sqlite3ConnectionString")]  
    public class FurionDbContext : AppDbContext<FurionDbContext>  
    {  
        public FurionDbContext(DbContextOptions<FurionDbContext> options) : base(options)  
        {  
        }  

        /// <summary>  
        /// 重写保存之前事件  
        /// </summary>  
        /// <param name="sender"></param>  
        /// <param name="e"></param>  
        protected override void SavingChangesEvent(DbContextEventData eventData, InterceptionResult<int> result)  
        {  
            // 获取当前事件对应上下文  
            var dbContext = eventData.Context;  

            // 强制重新检查一边实体更改信息  
            // dbContext.ChangeTracker.DetectChanges();  

            // 获取所有更改,删除,新增的实体,但排除审计实体(避免死循环)  
            var entities = dbContext.ChangeTracker.Entries()  
                .Where(u => u.Entity.GetType() != typeof(Audit) && (u.State == EntityState.Modified || u.State == EntityState.Deleted || u.State == EntityState.Added))  
                .ToList();  

            // 通过请求中获取当前操作人  
            var userId = App.GetService<IHttpContextAccessor>().HttpContext.Items["UserId"];  

            // 获取所有已更改的实体  
            foreach (var entity in entities)  
            {  
                // 获取实体类型  
                var entityType = entity.Entity.GetType();  

                // 获取所有实体有效属性,排除 [NotMapper] 属性  
                var props = entity.OriginalValues.Properties;  

                // 获取实体当前(现在)的值  
                var currentValues = entity.CurrentValues;  

                // 获取数据库中实体的值  
                var databaseValues = entity.GetDatabaseValues();  

                // 遍历所有属性  
                foreach (var prop in props)  
                {  
                    // 获取属性名  
                    var propName = prop.Name;  

                    // 获取现在的实体值  
                    var newValue = currentValues[propName];  

                    object oldValue = null;  
                    // 如果是新增数据,则 databaseValues 为空,所以需要判断一下  
                    if (databaseValues != null)  
                    {  
                        oldValue = databaseValues[propName];  
                    }  

                    // 插入审计日志表,Audit 是你自定义的实体  
                    dbContext.Set<Audit>().Add(new Audit  
                    {  
                        Table = entityType.Name,    // 表名  
                        Column = propName,  // 更新的列  
                        NewValue = newValue,    // 新值  
                        OldValue = oldValue,    // 旧值  
                        CreatedTime = DateTime.Now, // 操作时间  
                        UserId = userId,    // 操作人  
                        Operate = entity.State.ToString()  // 操作方式:新增、更新、删除  
                    });  
                }  
            }  
        }  
    }  
}  

小知识如果对性能有所要求,那么建议审计日志通过 日志组件 写入数据库,如通过 Nlog、Log4Net 这些等:

// 插入审计日志表  
dbContext.Set<Audit>().Add(new Audit  
{  
    Table = entityType.Name,    // 表名  
    Column = propName,  // 更新的列  
    newValue = newValue,    // 新值  
    OldValue = oldValue,    // 旧值  
    CreatedTime = DateTime.Now, // 操作时间  
    UserId = userId,    // 操作人  
    Operate = entity.State.ToString()  // 操作方式:新增、更新、删除  
});  

替换为:

logger.Information(JsonConvert.SerializeObject(new Audit  
{  
    Table = entityType.Name,    // 表名  
    Column = propName,  // 更新的列  
    newValue = newValue,    // 新值  
    OldValue = oldValue,    // 旧值  
    CreatedTime = DateTime.Now, // 操作时间  
    UserId = userId,    // 操作人  
    Operate = entity.State.ToString()  // 操作方式:新增、更新、删除  
}));  

通过上面的例子,我们就可以对数据库所有的新增、更新、删除进行监控了。

9.23.3.2 执行 sql 审计日志

主要通过 DbCommandInterceptor 拦截实现,具体使用可查看 数据库拦截器 - DbCommandInterceptor,如:

using Microsoft.EntityFrameworkCore.Diagnostics;  
using System.Data.Common;  
using System.Threading;  
using System.Threading.Tasks;  

namespace Furion.Web.Core  
{  
    /// <summary>  
    /// 执行 sql 审计  
    /// </summary>  
    public sealed class SqlCommandAuditInterceptor : DbCommandInterceptor  
    {  
        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)  
        {  
            // 获取执行的 sql 语句  
            var sql = command.CommandText;  

            // 获取执行的 sql 类型,是 sql 语句,还是存储过程,还是其他  
            var type = command.CommandType;  

            // 获取 sql 传递的命令参数  
            var parameters = command.Parameters;  

            // 写日志~~~~  

            return base.NonQueryExecuting(command, eventData, result);  
        }  

        public override ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)  
        {  
            // 获取执行的 sql 语句  
            var sql = command.CommandText;  

            // 获取执行的 sql 类型,是 sql 语句,还是存储过程,还是其他  
            var type = command.CommandType;  

            // 获取 sql 传递的命令参数  
            var parameters = command.Parameters;  

            // 写日志~~~~  

            return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);  
        }  

        // 其他 override  
    }  
}  

  • 注册审计日志

只需要在注册数据库上下文中指定 interceptors 参数即可

// services.AddDb 一样  
services.AddDbPool<FurionDbContext>(interceptors: new IInterceptor[] {  
    new SqlCommandAuditInterceptor()  
});  

9.23.3.3 请求审计日志

关于请求审计日志如需实现请求审计日志可查阅 【5.4 请求审计日志章节

9.23.4 反馈与建议

与我们交流给 Furion 提 Issue